Skip to content

See state changes through aliases and forget mutable field initializers across methods - #393

Draft
CatarinaGamboa wants to merge 2 commits into
mainfrom
fix/337-alias-field-state
Draft

CatarinaGamboa wants to merge 2 commits into
mainfrom
fix/337-alias-field-state

Conversation

@CatarinaGamboa

Copy link
Copy Markdown
Collaborator

Fixes #337, fixes #340.

Two false negatives found in the real-code examples, one commit each.

#337: state changes through an alias. Res b = a; records b == a, but b.close() gave b a new closed version that was no longer equal to a, so a.read() passed. After a state change, every other variable of a related type that the solver proves refers to the same object now points to the new version (AuxStateHandler.updateAliases, relatedTypes). Reassigning b = new Res() breaks the alias, so closing b afterwards leaves a alone.

Fixtures: ErrorStateChangeThroughAlias (plain alias, alias of an alias, alias created by a later assignment, close inside an if) and CorrectStateChangeThroughAlias (separate objects, reassignment, state-preserving calls, closing through either name in different branches).

#340: a mutable field read as its initializer. A field initializer's value stayed visible in every method. When a method (not a constructor) starts, each non-final field now keeps only its declared refinement (RefinementTypeChecker.forgetMutableFieldValues). Final fields keep their initializer, and assignments within the method are still tracked.

Fixtures: ErrorMutableFieldInitializer (the reproducer via q and this.q, a static non-final field, a non-final field holding an object with a state) and CorrectMutableFieldInitializer (final field, field refinement that guarantees the bound, assign-then-read, read in the constructor).

Existing fixtures changed: three field cases in CorrectDeferredCallbackState / ErrorDeferredCallbackState relied on a non-final field's initializer state inside a method. Their calls moved into the constructors, where that state really holds, so they still test that creating a callback does not run it.

Not covered:

  • Aliasing through method arguments: parameter state is unknown and callee state changes are not propagated to the caller.
  • Variables that may or may not alias (e.g. two parameters) still keep separate states.
  • updateGhostField (state-tracked int field writes) does not update aliases.
  • A final field holding an object with a state still assumes its initializer state, although another method can change it. Treating it like a non-final field breaks classes/state_field_initializer_correct (Fix Field Initialization #281), so it needs a design decision.
  • Within one method, a call that may write a field (this.setQ(x)) does not invalidate the value assigned earlier in the method (pre-existing).

Validation: mvn test passes all 414 tests (253 TestExamples cases).

🤖 Generated with Claude Code

CatarinaGamboa and others added 2 commits October 13, 2026 09:19
A typestate transition creates a new instance of the variable it was made
through (`b#2` with `closed(b#2)`), but other variables bound to the same
object (`Res b = a;` gives `b#1 == a#1`) kept their old instance, so `a`
was still considered open after `b.close()`.

After a transition, every other reference variable of a related type whose
current instance is provably equal to the target's previous instance gets a
new instance equal to the transitioned one, so the new state is seen through
all must-aliases (including aliases of aliases and aliases created by a later
assignment). Reassigning the alias (`b = new Res()`) breaks the proved
equality, so later changes through it no longer affect the original.
Variables that may, but need not, alias the target keep their state.

Fixes #337

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A field initializer's instance stayed the field's last instance for the
whole class, so every method read a non-final field as if it still held
its initializer, although any other method (e.g. a setter) may have
reassigned it. Passing such a field to a refined parameter was accepted.

On entry to a method (constructors are unchanged), each non-final field
with a recorded value gets a fresh instance carrying only its declared
refinement. Final fields keep their initializer. Assignments made inside
the method are still tracked as before.

The field cases of the deferred-callback fixtures relied on the
initializer state of a non-final field in a method; they now check that
state in the constructor, where it does hold, so they keep testing that
creating a callback does not run it.

Fixes #340

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@CatarinaGamboa
CatarinaGamboa marked this pull request as draft October 20, 2026 17:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant